package com.keguoyu.easymvp.ioc.origin

import com.android.build.api.transform.Status
import com.android.build.api.transform.TransformInput
import com.android.build.api.transform.TransformOutputProvider
import com.google.common.collect.ImmutableMap
import com.google.common.collect.ImmutableSet
import com.keguoyu.easymvp.ioc.internal.ClassMapping
import com.keguoyu.easymvp.ioc.internal.INVOKER_INFO_FILE_NAME
import com.keguoyu.easymvp.ioc.internal.Invocation
import com.keguoyu.easymvp.ioc.internal.InvokerInfos
import com.keguoyu.easymvp.ioc.internal.JarFileOutput
import com.keguoyu.easymvp.ioc.internal.util.loopAllFiles
import com.keguoyu.easymvp.ioc.internal.util.loopAllJars
import com.keguoyu.easymvp.ioc.internal.util.parseInvocation
import com.keguoyu.easymvp.ioc.internal.util.toClassPath
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking
import java.io.File
import java.io.FileReader
import java.io.InputStreamReader
import java.io.Reader
import java.util.*
import java.util.jar.JarFile
import kotlin.collections.HashMap
import kotlin.collections.HashSet

class FileScanner {

  private val mUnmatchedClasses = Collections.synchronizedSet(HashSet<String>())
  private val mInvocations = Collections.synchronizedSet(HashSet<Invocation>())

  private val mClassFileMapping = HashMap<String, String>()
  private val mClassJarMapping = HashMap<String, JarFileOutput>()

  fun scan(inputs: Collection<TransformInput>,
           outputProvider: TransformOutputProvider): Pair<InvokerInfos, FileSet> {
    val files = HashSet<File>()
    val jars = HashSet<JarFileOutput>()
    runBlocking {
      val jobs = LinkedList<Deferred<Unit>>()
      inputs.forEach {
        val roots = it.loopAllFiles(outputProvider) task@{ fin, fout, status ->
          if (status == Status.REMOVED) {
            return@task
          }
          jobs += async(Dispatchers.IO) {
            scanInvokerInfo(fin) reader@{ file ->
              if (file.absolutePath.endsWith(INVOKER_INFO_FILE_NAME)) {
                return@reader FileReader(file)
              }
              return@reader null
            }
          }
        }
        it.loopAllJars(outputProvider) task@{ fin, fout ->
          if (fin.status == Status.REMOVED) {
            return@task
          }
          jars.add(JarFileOutput(fout.absolutePath, fin.name))
          jobs += async {
            scanInvokerInfo(fin.file) reader@{ file ->
              val jar = JarFile(file)
              val entry = jar.getJarEntry(INVOKER_INFO_FILE_NAME)
              if (entry != null) {
                return@reader InputStreamReader(jar.getInputStream(entry))
              }
              return@reader null
            }
          }
        }
        files.addAll(roots)
      }
      jobs.awaitAll()
    }
    inputs.forEach {
      it.loopAllFiles(outputProvider) { fin, dest, _ ->
        scanClass(fin, { clazz ->
          mClassFileMapping[clazz] = dest.absolutePath
        }, { clazz ->
          absolutePath.endsWith(clazz) && exists()
        })
      }
      it.loopAllJars(outputProvider) { fin, dest ->
        scanClass(fin,
          { clazz ->
            mClassJarMapping[clazz] = JarFileOutput(dest.absolutePath, fin.name)
          },
          { clazz ->
            JarFile(file).getJarEntry(clazz) != null
          })
      }
    }
    return Pair(InvokerInfos(
        ClassMapping(
            ImmutableMap.copyOf(mClassJarMapping),
            ImmutableMap.copyOf(mClassFileMapping)),
        ImmutableSet.copyOf(mInvocations)),
        FileSet(files, jars))
  }

  private fun readMetaInfo(reader: Reader) {
    val (invocations, allClassSet) = reader.parseInvocation()
    mInvocations.addAll(invocations)
    mUnmatchedClasses.addAll(allClassSet)
  }

  private fun scanInvokerInfo(file: File, toReader: (File) -> Reader?) {
    val reader = toReader(file)
    if (reader != null) {
      println("invoker got meta from ${file.absolutePath}")
      readMetaInfo(reader)
    }
  }

  private fun <F> scanClass(file: F,
                            put: (String) -> Unit,
                            contains: F.(String) -> Boolean) {
    val matched = HashSet<String>()
    mUnmatchedClasses.forEach {
      val path = it.toClassPath()
      if (file.contains(path)) {
        put(it)
        matched.add(it)
      }
    }
    mUnmatchedClasses.removeAll(matched)
  }
}

data class FileSet(val files: Collection<File>, val jars: Collection<JarFileOutput>) {
  fun loopFiles(task: (File) -> Unit) {
    files.forEach(task)
  }

  fun loopJars(task: (JarFileOutput) -> Unit) {
    jars.forEach(task)
  }
}
